﻿namespace MvcScriptManager
{
    using System;
    using System.ComponentModel;
    using System.Globalization;
    using System.IO;
    using System.Security.Permissions;
    using System.Web;
    using System.Web.UI;

    /// <summary>
    /// Represents a script.
    /// </summary>
    [DefaultProperty("Path"), AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal), AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
    public class ScriptReference
    {
        /// <summary>
        /// The virtual path of the script.
        /// </summary>
        private string path;

        /// <summary>
        /// The assembly string representing an assemly where the script localization resource resides in.
        /// </summary>
        private string resourceAssembly;

        /// <summary>
        /// The name of the resource file where the script localization resource resides in.
        /// </summary>
        private string resourceName;

        /// <summary>
        /// The name of the type to create for the values in the resource file.
        /// </summary>
        private string resourceTypeName;

        /// <summary>
        /// A value indicating if the script content should be crunched when it is rendered.
        /// </summary>
        private bool enableCrunching;

        /// <summary>
        /// Initializes a new instance of the <see cref="ScriptReference"/> class.
        /// </summary>
        public ScriptReference()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ScriptReference"/> class.
        /// </summary>
        /// <param name="path">The virtual path of the script.</param>
        public ScriptReference(string path)
        {
            this.path = path;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ScriptReference"/> class.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <param name="resourceAssembly">The assembly string representing an assemly where the script localization resource resides in.</param>
        /// <param name="resourceName">The name of the resource file where the script localization resource resides in.</param>
        /// <param name="resourceTypeName">The name of the type to create for the values in the resource file.</param>
        public ScriptReference(string path, string resourceAssembly, string resourceName, string resourceTypeName)
        {
            this.path = path;
            if (!string.IsNullOrEmpty(resourceAssembly) && !string.IsNullOrEmpty(resourceName) && !string.IsNullOrEmpty(resourceTypeName))
            {
                this.resourceAssembly = resourceAssembly;
                this.resourceName = resourceName;
                this.resourceTypeName = resourceTypeName;
            }
        }

        /// <summary>
        /// Gets or sets the virtual path.
        /// </summary>
        /// <value>The virtual path.</value>
        [NotifyParentProperty(true), Category("Behavior"), DefaultValue(""), Description("The path to the script"), UrlProperty("*.js")]
        public string Path
        {
            get
            {
                if (this.path != null)
                {
                    return this.path;
                }

                return string.Empty;
            }

            set
            {
                this.path = value;
            }
        }

        /// <summary>
        /// Gets or sets the assembly where the script resource resides in.
        /// </summary>
        /// <value>The resource assembly.</value>
        [NotifyParentProperty(true), Category("Behavior"), DefaultValue(""), Description("The resource asssembly of the script")]
        public string ResourceAssembly
        {
            get
            {
                if (this.resourceAssembly == null)
                {
                    return string.Empty;
                }

                return this.resourceAssembly;
            }

            set
            {
                this.resourceAssembly = value;
            }
        }

        /// <summary>
        /// Gets or sets the name of the resource file where the script localization resource resides in.
        /// </summary>
        /// <value>The name of the resource file where the script localization resource resides in.</value>
        [NotifyParentProperty(true), Category("Behavior"), DefaultValue(""), Description("The name of the resource file")]
        public string ResourceName
        {
            get
            {
                if (this.resourceName == null)
                {
                    return string.Empty;
                }

                return this.resourceName;
            }

            set
            {
                this.resourceName = value;
            }
        }

        /// <summary>
        /// Gets or sets the name of the type to create for the values in the resource file.
        /// </summary>
        /// <value>The name of the type to create for the values in the resource file.</value>
        [NotifyParentProperty(true), Category("Behavior"), DefaultValue(""), Description("The name of the type to create for the values in the resource file")]
        public string ResourceTypeName
        {
            get
            {
                if (this.resourceTypeName == null)
                {
                    return string.Empty;
                }

                return this.resourceTypeName;
            }

            set
            {
                this.resourceTypeName = value;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the script content should be crunched when it is rendered.
        /// Default is false.
        /// </summary>
        /// <value><c>true</c> if the script content should be crunched when it is rendered; otherwise, <c>false</c>.</value>
        [NotifyParentProperty(true), Category("Behavior"), DefaultValue(false), Description("A boolean value indicating if the script content should be crunched when it is rendered")]
        public bool EnableCrunching
        {
            get
            {
                return this.enableCrunching;
            }

            set
            {
                this.enableCrunching = value;
            }
        }

        /// <summary>
        /// Gets the script content.
        /// </summary>
        /// <param name="localPath">The local path.</param>
        /// <returns>The script content.</returns>
        internal static string GetScript(string localPath)
        {
            return File.ReadAllText(localPath);
        }

        /// <summary>
        /// Gets the path on the local system.
        /// </summary>
        /// <param name="virtualPath">The virtual path.</param>
        /// <param name="context">The HTTP context.</param>
        /// <returns>The path on the local system.</returns>
        internal static string GetLocalPath(string virtualPath, HttpContext context)
        {
            if (string.IsNullOrEmpty(virtualPath))
            {
                throw new ArgumentNullException("virtualPath");
            }

            string localPath = context.Server.MapPath(GetScriptPath(virtualPath, false));
            if (!File.Exists(localPath))
            {
                localPath = context.Server.MapPath(virtualPath);
            }

            return localPath;
        }

        /// <summary>
        /// Gets the URL of the script that is usable on the requesting client.
        /// </summary>
        /// <param name="context">The HTTP context.</param>
		/// /// <param name="forceReleaseMode">Forces the URL to ignore debugging settings and return the 'production' filename</param>
        /// <returns>The URL of the script.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1055:UriReturnValuesShouldNotBeStrings", Justification = "Needs to be string.")]
        internal string GetUrl(HttpContext context, bool forceReleaseMode)
        {
            return context.Response.ApplyAppPathModifier(GetScriptPath(this.Path, forceReleaseMode));
        }

        /// <summary>
        /// Gets the script path.
        /// </summary>
        /// <remarks>
        /// Under debug mode, it is the debug virtual path; otherwise,
        /// it is the virtual path.
        /// </remarks>
        /// <param name="virtualPath">The virtual path.</param>
        /// <param name="forceReleaseMode">Forces the URL to ignore debugging settings and return the 'production' filename</param>
        /// <returns>The script path.</returns>
        private static string GetScriptPath(string virtualPath, bool forceReleaseMode)
        {
            return (ScriptManager.IsDebuggingEnabled && !forceReleaseMode) ? GetDebugPath(virtualPath) : virtualPath;
        }

        /// <summary>
        /// Gets the debug virtual path.
        /// </summary>
        /// <param name="virtualPath">The virtual path.</param>
        /// <returns>The debug virtual path.</returns>
        private static string GetDebugPath(string virtualPath)
        {
            string mainPath;
            string queryString;
            int index = virtualPath.IndexOf('?');
            if (index >= 0)
            {
                mainPath = virtualPath.Substring(0, index);
                queryString = virtualPath.Substring(index);
            }
            else
            {
                mainPath = virtualPath;
                queryString = null;
            }

            if (!mainPath.EndsWith(".js", StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, "'{0}' is not a valid script path.  The path must end in '.js'.", new object[] { mainPath }));
            }

            return ReplaceExtension(mainPath) + queryString;
        }

        /// <summary>
        /// Replaces the extension with debug flag.
        /// </summary>
        /// <param name="basePath">The base path (without query string).</param>
        /// <returns>The replaced path.</returns>
        private static string ReplaceExtension(string basePath)
        {
            return (basePath.Substring(0, basePath.Length - 2) + "debug.js");
        }
    }
}
